# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCharts import QChart, QChartView
from PySide6.QtGui import QKeyEvent, QMouseEvent, QFont, QPainterPath, QColor, QFontMetrics, QPainter, QPen
from PySide6.QtCore import QEvent, Qt, QRectF, QPointF, QRect, QDateTime
from PySide6.QtWidgets import QGraphicsView, QGraphicsTextItem, QGraphicsItem, QGraphicsLineItem


class Callout(QGraphicsItem):  # 显示浮动提示框
    def __init__(self, chart):
        QGraphicsItem.__init__(self, chart)
        self._chart = chart
        self._text = ""
        self._textRect = QRectF()
        self._anchor = QPointF()
        self._font = QFont()
        self._rect = QRectF()

    def boundingRect(self):  # 返回一个矩形区域，定义了该项在场景中的空间。根据锚点和矩形区域来计算边界。
        anchor = self.mapFromParent(self._chart.mapToPosition(self._anchor))
        rect = QRectF()
        rect.setLeft(min(self._rect.left(), anchor.x()))
        rect.setRight(max(self._rect.right(), anchor.x()))
        rect.setTop(min(self._rect.top(), anchor.y()))
        rect.setBottom(max(self._rect.bottom(), anchor.y()))
        return rect

    def paint(self, painter, option, widget):  # 绘制 Callout 的矩形框和文本，确保它能正确显示在图表上，并根据需要绘制连接线（锚点连接线）。
        path = QPainterPath()
        path.addRoundedRect(self._rect, 5, 5)
        anchor = self.mapFromParent(self._chart.mapToPosition(self._anchor))
        if not self._rect.contains(anchor) and not self._anchor.isNull():
            point1 = QPointF()
            point2 = QPointF()

            # establish the position of the anchor point in relation to _rect
            above = anchor.y() <= self._rect.top()
            above_center = (anchor.y() > self._rect.top() and anchor.y() <= self._rect.center().y())
            below_center = (anchor.y() > self._rect.center().y() and anchor.y() <= self._rect.bottom())
            below = anchor.y() > self._rect.bottom()

            on_left = anchor.x() <= self._rect.left()
            left_of_center = (anchor.x() > self._rect.left() and anchor.x() <= self._rect.center().x())
            right_of_center = (anchor.x() > self._rect.center().x() and anchor.x() <= self._rect.right())
            on_right = anchor.x() > self._rect.right()

            # get the nearest _rect corner.
            x = (on_right + right_of_center) * self._rect.width()
            y = (below + below_center) * self._rect.height()
            corner_case = ((above and on_left) or (above and on_right) or (below and on_left) or (below and on_right))
            vertical = abs(anchor.x() - x) > abs(anchor.y() - y)

            x1 = (x + left_of_center * 10 - right_of_center * 20 + corner_case * int(not vertical) * (
                        on_left * 10 - on_right * 20))
            y1 = (y + above_center * 10 - below_center * 20 + corner_case * vertical * (above * 10 - below * 20))
            point1.setX(x1)
            point1.setY(y1)

            x2 = (x + left_of_center * 20 - right_of_center * 10 + corner_case * int(not vertical) * (
                        on_left * 20 - on_right * 10))
            y2 = (y + above_center * 20 - below_center * 10 + corner_case * vertical * (above * 20 - below * 10))
            point2.setX(x2)
            point2.setY(y2)

            path.moveTo(point1)
            path.lineTo(anchor)
            path.lineTo(point2)
            path = path.simplified()

        painter.setBrush(QColor(255, 255, 255))
        painter.drawPath(path)
        painter.drawText(self._textRect, self._text)  #绘制气泡框

    def mousePressEvent(self, event):  # 允许用户拖动浮动框。
        event.setAccepted(True)

    def mouseMoveEvent(self, event):  # 允许用户拖动浮动框。
        if event.buttons() & Qt.LeftButton:
            self.setPos(self.mapToParent(
                event.pos() - event.buttonDownPos(Qt.LeftButton)))
            event.setAccepted(True)
        else:
            event.setAccepted(False)

    def set_text(self, text):  # 设置 Callout 的文本内容并计算其矩形区域。
        self._text = text
        metrics = QFontMetrics(self._font)
        self._textRect = QRectF(metrics.boundingRect(
            QRect(0.0, 0.0, 150.0, 150.0), Qt.AlignLeft, self._text))
        self._textRect.translate(5, 5)
        self.prepareGeometryChange()
        self._rect = self._textRect.adjusted(-5, -5, 5, 5)

    def set_anchor(self, point):  # 设置浮动框的锚点位置，这个位置决定了浮动框将如何相对于图表的某个点定位。
        self._anchor = QPointF(point)

    def update_geometry(self):  # 更新浮动框的几何形状，计算其位置和大小。
        self.prepareGeometryChange()
        if self._chart.mapToPosition(self._anchor).x() > 1050:
            self.setPos(self._chart.mapToPosition(
                self._anchor) + QPointF(-120, -50))
        else:
            self.setPos(self._chart.mapToPosition(
                self._anchor) + QPointF(10, -50))


# 实现了对鼠标事件、触摸事件、键盘事件的自定义处理。QChartView 是 Qt 图表模块 QtCharts 中用于显示和交互操作图表的视图类。
class ChartView(QChartView):
    def __init__(self, chart, key_data, shuju_zidian, y_max, parent=None):
        super().__init__(chart, parent)
        self.key_v_data = key_data  # 第一项为项目名称，第二项为选择结果。数据项多选结果
        self.shuju_zidian = shuju_zidian  # 数据存储结果
        self._tooltip = Callout(chart)  # 创建一个 Callout 对象，用于显示浮动提示框。
        self.chart1 = chart  # 存储本地Qchart对象
        self.closest_key = None  # 存储鼠标识别点X轴位置
        self.time_key = None  # 初始化完之后最后一项时间戳
        self._vertical_line = None  # 用于存储竖杠的引用
        self.y_max = y_max  # 初始化Y轴范围最大值，用来确定云框显示位置
        # 这行代码启用了“橡皮筋选择”功能，允许用户在图表区域用鼠标框选一个矩形区域。通过 RectangleRubberBand，用户可以选择一个矩形区域，然后执行拖动或缩放等操作。
        self.setRubberBand(QChartView.RectangleRubberBand)
        self._isTouching = False
        self.setRenderHint(QPainter.Antialiasing)
        self.setDragMode(QChartView.NoDrag)

        self._lastMousePosition = QPointF()
        self._middleButtonPressed = False

    def draw_vertical_line(self, x_pos):  # 竖杠显示函数
        if self._vertical_line is not None:
            self.scene().removeItem(self._vertical_line)
            del self._vertical_line
            self._vertical_line = None

        line = QGraphicsLineItem(x_pos, 0, x_pos, self.height())
        line.setPen(QPen(Qt.red, 1))  # 设置竖杠的颜色和宽度
        self.scene().addItem(line)
        self._vertical_line = line

    def viewPortEvent(self, event: QEvent):  # 触摸事件处理
        if event.type() == QMouseEvent.TouchBegin:
            self._isTouching = True
            self.chart().setAnimationOptions(QChart.NoAnimation)  # 禁用动画可以使得触摸操作更加流畅，不会受到动画效果的干扰。
        return super().viewPortEvent(event)

    def mousePressEvent(self, event: QMouseEvent):  # 鼠标按下事件
        if self._isTouching:
            return

        if event.button() == Qt.MouseButton.MiddleButton:
            self._middleButtonPressed = True
            self._lastMousePosition = event.position()
            event.accept()
        else:
            return super().mousePressEvent(event)  # 如果没有触摸操作，则调用父类的 mousePressEvent 方法，执行标准的鼠标按下事件处理。

    # 显示提示框
    def show_yun(self, time):
        if self._tooltip is None:
            self._tooltip = Callout(self.chart1)
        print_n = {}
        x_value1 = QDateTime.fromMSecsSinceEpoch(int(time)).toString('hh:mm:ss.zzz')
        for i, data_y1 in enumerate(self.key_v_data):  # 遍历多选对象
            if self.key_v_data[i][1] == True:  # 判断多选
                # 使用 min() 找到与查询键差异最小的键
                self.closest_key = min(self.shuju_zidian[self.key_v_data[i][0]], key=lambda k: abs(k - time))
                # print("closest_key",self.closest_key)
                if abs(self.closest_key - time) > 1000:
                    print_n[self.key_v_data[i][0]] = None
                else:
                    print_n[self.key_v_data[i][0]] = self.shuju_zidian[self.key_v_data[i][0]][self.closest_key]
        # closest_key 为当前识别点
        # 如果没有文本项，就创建一个新的
        prtext = "时间:" + x_value1
        for key, value in print_n.items():
            if value == None:
                prtext = prtext + "\n" + key + ":None"
            else:
                prtext = prtext + "\n" + key + f": {value:.2f}"
        self._tooltip.set_text(prtext)

        self._tooltip.set_anchor(QPointF(time, self.y_max / 2))
        self._tooltip.setZValue(11)
        self._tooltip.update_geometry()
        self._tooltip.show()
        # 绘制竖杠
        self.draw_vertical_line(self.chart().mapToPosition(QPointF(time, 1300)).x())

    # 隐藏提示框
    def close_yun(self):
        if self._tooltip is None:
            return
        self._tooltip.scene().removeItem(self._tooltip)  # 删除云框对象
        self._tooltip.setParentItem(None)
        del self._tooltip
        self._tooltip = None
        self.closest_key = None
        # 移除竖杠
        if self._vertical_line is not None:
            self.scene().removeItem(self._vertical_line)
            del self._vertical_line
            self._vertical_line = None

    def mouseMoveEvent(self, event: QMouseEvent):  # 鼠标移动事件
        if self._isTouching:
            return
        if self._middleButtonPressed:
            delta = event.position() - self._lastMousePosition
            self.chart().scroll(-delta.x(), delta.y())
            self._lastMousePosition = event.position()
            event.accept()
        else:
            mouse_pos = event.pos()  # 获取鼠标在视图中的位置
            print_n = {}

            # 将视图坐标转换为图表坐标
            chart_pos = self.chart().mapToValue(mouse_pos)
            # print("1    ", chart_pos)
            # print("2    ", self.chart().mapToPosition(chart_pos).x())
            # print("3    ", mouse_pos)
            if self._tooltip is None:
                self._tooltip = Callout(self.chart1)
            # 获取鼠标位置对应的 y 值
            x_value = int(chart_pos.x())
            x_value1 = QDateTime.fromMSecsSinceEpoch(x_value).toString('hh:mm:ss.zzz')
            for i, data_y1 in enumerate(self.key_v_data):  # 遍历多选对象
                if self.key_v_data[i][1] == True:  # 判断多选
                    # 使用 min() 找到与查询键差异最小的键
                    self.closest_key = min(self.shuju_zidian[self.key_v_data[i][0]], key=lambda k: abs(k - x_value))
                    # print("closest_key",self.closest_key)
                    if abs(self.closest_key - x_value) > 1000:
                        print_n[self.key_v_data[i][0]] = None
                    else:
                        print_n[self.key_v_data[i][0]] = self.shuju_zidian[self.key_v_data[i][0]][self.closest_key]
            # closest_key 为当前识别点
            # 如果没有文本项，就创建一个新的
            prtext = "时间:" + x_value1
            for key, value in print_n.items():
                if value == None:
                    prtext = prtext + "\n" + key + ":None"
                else:
                    prtext = prtext + "\n" + key + f": {value:.2f}"
            self._tooltip.set_text(prtext)
            self._tooltip.set_anchor(chart_pos)
            self._tooltip.setZValue(11)
            self._tooltip.update_geometry()
            self._tooltip.show()
            # 绘制竖杠
            self.draw_vertical_line(mouse_pos.x())
            return super().mouseMoveEvent(event)  # 调用父类的 mouseMoveEvent 处理标准的鼠标移动逻辑。

    # def enterEvent(self, event):
    #     # 鼠标进入时触发
    #     print("鼠标进入图表区域")
    #     super().enterEvent(event)  # 调用父类方法保持原有行为

    def leaveEvent(self, event):  # 离开窗口
        # 鼠标离开时触发
        self._tooltip.scene().removeItem(self._tooltip)  # 删除云框对象
        self._tooltip.setParentItem(None)
        del self._tooltip
        self._tooltip = None
        self.closest_key = None

        # 移除竖杠
        if self._vertical_line is not None:
            self.scene().removeItem(self._vertical_line)
            del self._vertical_line
            self._vertical_line = None
        super().leaveEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent):  # 鼠标释放事件
        if self._isTouching:
            self._isTouching = False

        if event.button() == Qt.MouseButton.MiddleButton:
            self._middleButtonPressed = False
            event.accept()
        else:
            self.chart().setAnimationOptions(QChart.SeriesAnimations)  # 启用图表的动画效果（通过 QChart.SeriesAnimations）。
            return super().mouseReleaseEvent(event)  # 调用父类的 mouseReleaseEvent，执行标准的鼠标释放事件处理。

    def wheelEvent(self, event):
        # 获取滚轮的增量
        angle = event.angleDelta().y()

        # 判断滚轮方向
        factor = 1.2 if angle > 0 else 0.8  # 向上滚动放大，向下滚动缩小

        # 获取图表的当前缩放比例并进行缩放
        self.chart().zoom(factor)

        # 事件处理完后，调用基类的 wheelEvent，以便进一步处理
        super().wheelEvent(event)

    def keyPressEvent(self, event: QKeyEvent):  # 键盘按下事件

        key = event.key()
        if key == Qt.Key_PageUp:
            self.chart().zoomIn()

        elif key == Qt.Key_PageDown:
            self.chart().zoomOut()

        elif key == Qt.Key_Left:
            self.chart().scroll(-10, 0)

        elif key == Qt.Key_Right:
            self.chart().scroll(10, 0)

        elif key == Qt.Key_Up:
            self.chart().scroll(0, 10)

        elif key == Qt.Key_Down:
            self.chart().scroll(0, -10)

        else:
            QGraphicsView.keyPressEvent(event)  # 如果按下的键不是上述几种，则调用父类的 QGraphicsView.keyPressEvent(event) 处理其他键盘事件。
